/*
Adobe Systems Incorporated(r) Source Code License Agreement
Copyright(c) 2007 Adobe Systems Incorporated. All rights reserved.
	
Please read this Source Code License Agreement carefully before using
the source code. Use of the APIs is governed by the Share Beta Services Agreement:
https://share.adobe.com/adc/tou.do.
	
Adobe Systems Incorporated grants to you a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license, to reproduce,
prepare derivative works of, publicly display, publicly perform, and
distribute this source code and such derivative works in source or
object code form without any attribution requirements.
	
The name "Adobe Systems Incorporated" must not be used to endorse or promote products
derived from the source code without prior written permission.
	
You agree to indemnify, hold harmless and defend Adobe Systems Incorporated from and
against any loss, damage, claims or lawsuits, including attorney's
fees that arise or result from your use or distribution of the source
code.
	
THIS SOURCE CODE IS PROVIDED "AS IS" AND "WITH ALL FAULTS", WITHOUT
ANY TECHNICAL SUPPORT OR ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ALSO, THERE IS NO WARRANTY OF
NON-INFRINGEMENT, TITLE OR QUIET ENJOYMENT. IN NO EVENT SHALL ADOBE
OR ITS SUPPLIERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOURCE CODE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.adobe.share.api
{
	import com.adobe.crypto.MD5;
	import com.adobe.utils.XMLUtil;
	
	import flash.events.DataEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.net.FileReference;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.net.URLRequestHeader;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;
	
	import mx.rpc.events.FaultEvent;
	
	/**
	 * ShareAPI is the helper class for accessing the Adobe Share API. It 
	 * provides much of the client-side functionality for applications; 
	 * for example, logging in and out, uploading files, sharing files, 
	 * and so on. While many of these functions call functions in the 
	 * ShareAPINode and ShareAPIUser classes, most of the work is done here. 
	 * These functions build the requests with the appropriate XML payload 
	 * and parse the responses.
	 */
	 
	public class ShareAPI extends EventDispatcher
	{	
		/**
		 * DEFAULT_ENDPOINT is the base URL for Share requests. The path is 
		 * extended with dc, auth, or sessions, depending on the nature of 
		 * the request.
		 */
		private var DEFAULT_ENDPOINT : String = "https://api.share.acrobat.com/webservices/api/v1/";
		
		/**
		 * auth is appended to DEFAULT_ENDPOINT for authentication requests during login.
		 */
		private var ENDPOINT_AUTH : String;
		
		/**
		 * sessions is appended to DEFAULT_ENDPOINT after authentication so that a 
		 * session ID can be obtained and used to track the current session.
		 */
		private var ENDPOINT_SESSIONS : String;
		
		/**
		 * dc is appended to DEFAULT_ENDPOINT to manipulate the specified node and 
		 * is usually accompanied by a node ID.
		 */
		private var ENDPOINT_DC : String;
		
		
		private var apikey : String = null;
		private var secret : String = null;
		
		// add to calltime milliseconds to prevent duplicate values
		private var callCount : int = 0;
		
		/**
		 * Share API constructor. Note that the developer must have registered 
		 * with Share and will need to have an API key and secret to continue.
		 * 
		 * @param apikey The API key registered for this application
		 * @param secret The shared secret assigned to this API key
		 */
		public function ShareAPI(apikey : String, secret : String) {
			this.ENDPOINT_AUTH = DEFAULT_ENDPOINT + "auth/";
			this.ENDPOINT_SESSIONS = DEFAULT_ENDPOINT + "sessions/";
			this.ENDPOINT_DC = DEFAULT_ENDPOINT + "dc/";
			
			this.apikey = apikey;
			this.secret = secret;
			
			// removes extraneous spaces and newlines from XML strings
			XML.prettyPrinting = false;
		}
		
		/**
		 * login constructs a log in request so the user can login to Share.
		 * If successful, user object will have its session ID and secret 
		 * attributes set.
		 * 
		 * @param user The user to log in
		 */
		public function login(user : ShareAPIUser) : ShareAPIToken {
			var urlLoader : URLLoader = new URLLoader();
			
			var x : XML = 	<request>
								<username>{user.username}</username>
								<password>{user.password}</password>
							</request>;
			
			var urlRequest : URLRequest = createRequest(user, "POST", ENDPOINT_AUTH, true, x);
			var token : ShareAPIToken = makeRequest(urlRequest)
						
			token.addEventListener(ShareAPIEvent.API_RESULT,
				function(event : ShareAPIEvent) : void {
					x =	<request>
							<authtoken>{event.payload.authtoken.toString()}</authtoken>
						</request>
					var urlRequest2 : URLRequest = createRequest(user, "POST", ENDPOINT_SESSIONS, true, x);		
					makeRequest(urlRequest2).addEventListener(ShareAPIEvent.API_RESULT,
						function(event2 : ShareAPIEvent) : void {
							user.sessionId = event2.payload.sessionid.toString();
							user.secret = event2.payload.secret.toString();
							user.name = event2.payload.name.toString();
							user.level = event2.payload.level;
							token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_AUTHENTICATED, null, null, null));
						});
				});
			
			token.user = user;
			return token;
		}
		
		/**
		 * logout logs a user out by ending the current session. In this example, 
		 * the session is ended by constructing an HTTP request using the DELETE 
		 * method and the current session ID.
		 * 
		 * @param user The user to log out
		 */
		public function logout(user : ShareAPIUser) : ShareAPIToken {
			var urlRequest : URLRequest = null;
			
			urlRequest = createRequest(user, "DELETE", ENDPOINT_SESSIONS + user.sessionId + "/", false, null);
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			return token;
		}
		
		/**
		 * Gets a list of nodes (files and folders) in a user's account. 
		 * Share returns a substantial amount of detail about each node,
		 * and getNodes calls functions in ShareAPINode to parse the data.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param nodeId List nodes that are children of this node. If this is null, use user's root node as parent.
		 * 
		 */
		public function getNodes(user : ShareAPIUser, nodeId : String = null) : ShareAPIToken {				
			var urlRequest : URLRequest = null;
			
			if(nodeId != null) {
				urlRequest = createRequest(user, "GET", ENDPOINT_DC + nodeId + "/", false, null);
			} else {
				urlRequest = createRequest(user, "GET", ENDPOINT_DC, false, null);
			}
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			token.user = user;
			token.nodeId = nodeId;
			return token;
		}
		
		/**
		 * Gets the details of the requested node from the Share response.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param nodeId Node to get the details about
		 * 
		 */
		public function getNodeDetails(user : ShareAPIUser, nodeId : String) : ShareAPIToken {				
			var urlRequest : URLRequest = null;
			
			urlRequest = createRequest(user, "GET", ENDPOINT_DC + nodeId + "/", false, null);
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			token.user = user;
			token.nodeId = nodeId;
			return token;
		}
		
		/**
		 * Returns the thumbnail of a document as a stream. Note that folders do not have 
		 * thumbnails, and not all files will have thumbnails.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param nodeId Node to get thumbnail of
		 * 
		 */
		public function getThumbnail(user : ShareAPIUser, nodeId : String) : ShareAPIToken {
			var urlRequest : URLRequest = null;
			
			urlRequest = createRequest(user, "GET", ENDPOINT_DC + nodeId + "/thumbnail/", false, null);
			
			var token : ShareAPIToken = makeRequest(urlRequest, "binary");
			token.user = user;
			token.nodeId = nodeId;
			return token;
		}
		
		/**
		 * Deletes a file or folder node node by constructing an HTTP request 
		 * using the DELETE method and the requisite node ID.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param nodeId Node to delete
		 * 
		 */
		public function deleteNode(user : ShareAPIUser, nodeId : String) : ShareAPIToken {				
			var urlRequest : URLRequest = null;
			
			urlRequest = createRequest(user, "DELETE", ENDPOINT_DC + nodeId + "/", false, null);
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			token.user = user;
			token.nodeId = nodeId;
			return token;
		}
		
		/**
		 * Shares an existing file or node with one or more specified users. The share level 
		 * is set by setting level to 0, 1, or 2. For example, this code could take in a 
		 * value set from a client's user interface, builds an XML payload containing a 
		 * list of specified users (if any), a message (which can be null), and the share 
		 * level. It then constructs an HTTP request using the PUT method and the path to 
		 * the requisite file.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param nodeId The file node to share
		 * @param users The list of users to share with
		 * @param emailMessage Optional message to include in email
		 * @param level 0 for private, 1 for enumerated users, 2 for public access
		 */
		public function shareFile(user : ShareAPIUser, nodeId : String, users : Array, emailMessage : String, level : int) : ShareAPIToken {				
			var urlRequest : URLRequest = null;
			
			var usersString : String = "";
			var share : XML = <share />
			for each(var userString : String in users) {
				share.appendChild(<user>{userString}</user>);
			}
			
			var x : XML = 	<request>
								<message>{emailMessage}</message>
								<level>{level}</level>
							</request>;
			x.appendChild(share);
			
			urlRequest = createRequest(user, "POST", ENDPOINT_DC + nodeId + "/share/", false, x);
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			token.user = user;
			token.nodeId = nodeId;
			token.users = users;
			token.emailMessage = emailMessage;
			token.level = level;
			return token;
		}
		
		/**
		 * Updates a shared file by adding and removing users with permission to access the file. 
		 * It builds an XML payload containing a list of specified users to add and remove and 
		 * then constructs an HTTP request using the PUT method and the path to the requisite file.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param nodeId File node to the share
		 * @param usersToAdd Add these users to the share	
		 * @param usersToRemove Remove these users from the share
		 * @param emailMessage Optional message to include in the email
		 */
		public function updateShare(user : ShareAPIUser, nodeId : String, usersToAdd : Array, usersToRemove : Array, emailMessage : String) : ShareAPIToken {				
			var urlRequest : URLRequest = null;
			
			var userString : String = null;
			var share : XML = <share />
			var usersToAddString : String = "";
			
			for each(userString in usersToAdd) {
				share.appendChild(<user>{userString}</user>);		
			}
			
			var unshare : XML = <unshare />
			var usersToRemoveString : String = "";
			
			for each(userString in usersToRemove) {
				unshare.appendChild(<user>{userString}</user>);		
			}
			
			var x : XML = 	<request>
								<message>{emailMessage}</message>
							</request>;
			x.appendChild(share);
			x.appendChild(unshare);
			
			urlRequest = createRequest(user, "POST", ENDPOINT_DC + nodeId + "/share/", false, null);
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			token.user = user;
			token.nodeId = nodeId;
			token.usersToAdd = usersToAdd;
			token.usersToRemove = usersToRemove;
			token.emailMessage = emailMessage;
			return token;
		}
		

		/**
		 * Adds a new folder to Share by building a request with an XML payload 
		 * containing the requisite details.
		 * 
		 * @param user The user performing that is executing this action
		 * @param name The name of the new folder
		 * @param description The new folder's description. 
		 * @param parentNode The target parent node under which to add this folder. If set to null, the folder is added to the root node.
		 */
		public function addFolder(user : ShareAPIUser, folderName : String, folderDescription : String, parentId : String) : ShareAPIToken {
			var urlRequest : URLRequest = null;
			
			var x : XML = 	<request>
								<folder>
									<name>{folderName}</name>
									<description>{folderDescription == null ? "" : folderDescription}</description>
								</folder>
							</request>;
								
			if(parentId != null) {
				urlRequest = createRequest(user, "POST", ENDPOINT_DC + parentId + "/", false, x);
			} else {
				urlRequest = createRequest(user, "POST", ENDPOINT_DC, false, x);
			}
			
			var token : ShareAPIToken = makeRequest(urlRequest);
			token.user = user;
			token.folderName = folderName;
			token.folderDescription = folderDescription;
			token.parentId = parentId;
			return token;
		}

		/**
		 * Uploads and adds a file to Share by building a request with an XML payload containing 
		 * the requisite details. Note that sig is the MD5 digest of the string formed by 
		 * concatenating data with the shared secret returned by an authorization token request.
		 * 
		 * @param user Execute method on behalf of this user
		 * @param file File to upload
		 * @param name Upload file using this name
		 * @param description Description of file. Set to null for none.
		 * @param parentNode Upload file under this folder node. If null, upload to root node.
		 * @param withRenditions Set to true to generate renditions (thumbnail, flash preview) for this document
		 */
		public function uploadFile(user : ShareAPIUser, file : FileReference, nodeId : String, name : String, description : String, renditions : Boolean, createpdf : Boolean = false) : void { 
			var urlRequest : URLRequest = new URLRequest();
			
			var requestBody : XML =	<request>
										<file>
											<name>{name}</name>
											<description>{description}</description>
											<renditions>{renditions}</renditions>
											<createpdf>{createpdf}</createpdf>
										</file>
									</request>
									
			var url : String = null;
			if(nodeId == null) {
				url = ENDPOINT_DC;
			} else {
				url = ENDPOINT_DC + nodeId;
			}

			var calltime : String = (((new Date()).time + callCount).toString());
			
			urlRequest.url = url + "?sessionid=" + user.sessionId + "&apikey=" + apikey + "&calltime=" + calltime;
			urlRequest.method = URLRequestMethod.POST;
			
			var dataString : String = urlRequest.method + " " + "/webservices/api/v1/dc/" + " apikey=" + apikey + " calltime=" + calltime + " sessionid=" + user.sessionId;
			trace(dataString);
			var sig : String = generateSig(user, dataString);
			
			urlRequest.url = urlRequest.url + "&sig=" + sig;
			
			urlRequest.data = new URLVariables();
			urlRequest.data.request = requestBody.toXMLString();
			
			try {
				file.upload(urlRequest);
			} catch(error : Error) {
				file.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_FAILURE, null, null, error.message));
			}
			
			file.addEventListener(DataEvent.UPLOAD_COMPLETE_DATA,
				function(event : DataEvent) : void {
					trace(event.data);
					file.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_RESULT, parseResult(event.data), null, null));
				});
				
			callCount++;
		}	
		
		/**
		 * downloadFile constructs an url request containing the node URL, session information, and 
		 * user details. Note that sig is the MD5 digest of the string formed by 
		 * concatenating data with the shared secret returned by an authorization token request.
		 * 
		 * @param user The user who is downloading the file
		 * @param nodeId The nodeId of the file to download
		 */
		public function downloadFile(user : ShareAPIUser, nodeId : String) : ShareAPIToken {
			var urlRequest : URLRequest = new URLRequest();
								
			var url : String = ENDPOINT_DC + nodeId + "/src/";

			var calltime : String = (((new Date()).time + callCount).toString());
			
			urlRequest.url = url + "?sessionid=" + user.sessionId + "&apikey=" + apikey + "&calltime=" + calltime;
			urlRequest.method = URLRequestMethod.GET;
			
			var dataString : String = urlRequest.method + " " + "/webservices/api/v1/dc/" + nodeId + "/src/" + " apikey=" + apikey + " calltime=" + calltime + " sessionid=" + user.sessionId;
			trace(dataString);
			var sig : String = generateSig(user, dataString);
			
			urlRequest.url = urlRequest.url + "&sig=" + sig;
			urlRequest.method = "GET";
			var token : ShareAPIToken = makeRequest(urlRequest, "binary", true);
			token.nodeId = nodeId;
		
			callCount++;
			
			return token;
		} 


		/**
		 * Make API request, parse results, and dispatch appropriate events.
		 * 
		 * @param urlRequest The previously constructed URL request which is used access a node
		 * @param format The format of the urlRequest (text)
		 * @param progress The request's progress 
		 */
		private function makeRequest(urlRequest : URLRequest, format : String = URLLoaderDataFormat.TEXT, progress : Boolean = false) : ShareAPIToken {
			var urlLoader : URLLoader = new URLLoader();
			urlLoader.dataFormat = format;
			
			var token : ShareAPIToken = new ShareAPIToken(null);
			
			urlLoader.addEventListener(Event.COMPLETE,
				function(event : Event) : void {
					trace(token.status);
					if(token.status != 200) {
						// Return error response
						token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_FAILURE, null, null, urlLoader.data));
					} else {
						// Return XML or binary response
						switch(urlLoader.dataFormat) {
							case URLLoaderDataFormat.TEXT:
								trace(urlLoader.data);
								try {
									var res : XML = parseResult(urlLoader.data);
									token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_RESULT, res, null, null));
								} catch(e : Error) {
									token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_FAILURE, null, null, e.message));
								}
								break;
								
							case URLLoaderDataFormat.BINARY:
								trace('binary');
								var bytes : ByteArray = urlLoader.data;
								token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_RESULT, null, bytes, null));
								break;
						}
					}
				});
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR,
				function(event : IOErrorEvent) : void {
					token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_FAILURE, null, null, event.text));
				});
			urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS,
				function(event : HTTPStatusEvent) : void {
					token.status = event.status;
				});
			if(progress) {
				urlLoader.addEventListener(ProgressEvent.PROGRESS,
					function(event : ProgressEvent) : void {
						token.dispatchEvent(event);
					});
			}
			urlLoader.load(urlRequest);

			callCount++;
			
			return token;
		}
		
		/**
		 * Parses the XML result returned in Share's response to a request and 
		 * throws an error if the result is invalid.
		 * 
		 * @param result The returned XML
		 */
		private function parseResult(result : Object) : XML {
			trace(result);
			if(XMLUtil.isValidXML(result.toString())) {
				var resultXML : XML = XML(result);
				if(resultXML.@status == "ok") {
					return resultXML;
				} else {
					throw Error(resultXML.message.toString());
				}
			} else {
				throw Error(result.toString());
			}
		}
		
		/**
		 * Creates an error string if an error occurs.
		 * 
		 * @param event The error event
		 */
		private function onFault(event : FaultEvent) : void {
			event.token.dispatchEvent(new ShareAPIEvent(ShareAPIEvent.API_FAILURE, null, null, event.message.toString()));
		}
		

		
		/**
		 * A helper function to create API requests
		 * 
		 * @param user The user making the request
		 * @param method The HTTP method used for the request
		 * @param url The URL to the node used in the request
		 * @param anon true or false
		 * @param requestBody The XML payload used in the request
		 */
		private function createRequest(user: ShareAPIUser, method : String, url : String, anon : Boolean, requestBody : XML) : URLRequest {
			var urlRequest : URLRequest = new URLRequest();
			urlRequest.url = url;
			urlRequest.method = method.toUpperCase();
			urlRequest.cacheResponse = false;
					
			if(method == "GET" ) {
				urlRequest.contentType = "application/x-www-form-urlencoded";
			} else if(method == "POST") {
				urlRequest.contentType = "application/xml";
			} else if(method == "PUT") {
				urlRequest.contentType = "application/xml";
			}
			
			urlRequest.requestHeaders.push(new URLRequestHeader("Authorization", generateAuthorization(user, method, url, anon)));			
			if(requestBody != null) urlRequest.data = requestBody.toXMLString();
			
			return urlRequest;
		}

		/**
		 * Generates the authorization header string. The string includes the session ID, API key, 
		 * a signatures, and other information. Note that sig is the MD5 digest of the string formed by 
		 * concatenating data with the shared secret returned by an authorization token request.
		 * 
		 * @param user The user making the request
		 * @param method The HTTP method used for the request
		 * @param url The URL to the node used in the request
		 * @param anon true or false
		 */		
		private function generateAuthorization(user : ShareAPIUser, method : String, url : String, anon : Boolean) : String {
			var authString : String = "AdobeAuth ";			
			if(!anon && user.sessionId != null) {
				authString += ("sessionid=\"" + user.sessionId + "\",");
			}
			authString += ("apikey=\"" + apikey + "\",");
			var calltime : String = ((new Date()).time + callCount).toString();
			var method : String = method;
			var url : String = url;
			var data : String = (method + " " + url + " " + calltime);
			authString += ("data=\"" + data + "\",");
			var sig : String = generateSig(user, data);
			authString += ("sig=\"" + sig + "\"");
			return authString;
		}
		
		/**
		 * Generates an MD5 signature signature hash as per the API documentation. As with the 
		 * HTTP authorization header, the signature is formed by taking the MD5 digest of the 
		 * concatenated data string and the shared secret.
		 * 
		 * @param user The user making the request that requires authorization
		 * @param data 	A concatenation of three values separated by spaces: the HTTP method, the URL of the request, and the timestamp of the request in milliseconds.
		 */
		private function generateSig(user : ShareAPIUser, data : String) : String {
			if(user.secret == null) {
				return MD5.hash(data + secret);
			} else {
				return MD5.hash(data + user.secret);
			}
		}
	}
}
